package cn.lingyangwl.agile.system.service.impl;

import cn.lingyangwl.agile.auth.*;
import cn.lingyangwl.agile.common.core.exception.enums.*;
import cn.lingyangwl.agile.common.security.utils.*;
import cn.lingyangwl.agile.dynamic.config.core.*;
import cn.lingyangwl.agile.infra.*;
import cn.lingyangwl.agile.infra.module.group.model.rqrs.*;
import cn.lingyangwl.agile.model.*;
import cn.lingyangwl.agile.model.constants.*;
import cn.lingyangwl.agile.model.enums.*;
import cn.lingyangwl.agile.model.module.config.*;
import cn.lingyangwl.agile.model.module.config.param.*;
import cn.lingyangwl.agile.model.module.tenant.*;
import cn.lingyangwl.agile.model.utils.*;
import cn.lingyangwl.agile.msg.push.api.*;
import cn.lingyangwl.agile.msg.push.api.model.rqrs.*;
import cn.lingyangwl.agile.system.assembly.*;
import cn.lingyangwl.agile.system.constants.*;
import cn.lingyangwl.agile.system.handler.config.*;
import cn.lingyangwl.agile.system.manager.*;
import cn.lingyangwl.agile.system.mapper.*;
import cn.lingyangwl.agile.system.model.entity.*;
import cn.lingyangwl.agile.system.model.rqrs.config.*;
import cn.lingyangwl.agile.system.service.*;
import cn.lingyangwl.agile.tenant.core.utils.*;
import cn.lingyangwl.framework.core.utils.spring.*;
import cn.lingyangwl.framework.data.mate.annotations.*;
import cn.lingyangwl.framework.mybatis.plus.utils.*;
import cn.lingyangwl.framework.tool.core.*;
import cn.lingyangwl.framework.tool.core.exception.*;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

/**
 * @author shenguangyang
 */
@Slf4j
@Service
public class SysConfigServiceImpl extends ServiceImpl<SysConfigMapper, SysConfig> implements SysConfigService {
    @Resource
    private SysConfigManager configManager;
    @Resource
    private SysConfigAssembly configAssembly;
    @Resource
    private MsgPushApi msgPushApi;
    @Resource
    private DynamicConfigService configEventService;
    @Resource
    private RedisTemplate<String, Object> redisTemplate;
    @Resource
    private InfraApi infraApi;
    @Resource
    private AuthApi authApi;

    @Override
    @FieldBindHandle
    public IPage<ConfigResp> pageConfig(ConfigPageReq req) {
        LocalDateTime beginTime = req.getBeginLocalDateTime();
        LocalDateTime endTime = req.getEndLocalDateTime();

        LambdaQueryWrapper<SysConfig> queryWrapper = SysConfig.lqw()
                .like(StringUtils.isNotEmpty(req.getConfigName()), SysConfig::getConfigName, req.getConfigName())
                .like(StringUtils.isNotEmpty(req.getConfigKey()), SysConfig::getConfigKey, req.getConfigKey())
                .eq(Objects.nonNull(req.getSysBuiltIn()), SysConfig::getSysBuiltIn, req.getSysBuiltIn())
                .eq(Objects.nonNull(req.getGroupId()), SysConfig::getGroupId, req.getGroupId())
                .eq(SysConfig::getIsCustom, true)
                .ge(Objects.nonNull(beginTime), BasePlusPO::getCreatedAt, beginTime)
                .le(Objects.nonNull(endTime), BasePlusPO::getCreatedAt, endTime)
                .orderByDesc(SysConfig::getGroupId).orderByDesc(BasePlusPO::getCreatedAt);
        IPage<SysConfig> pageData = this.page(PageUtil.to(req), queryWrapper);
        List<Long> groupIds = pageData.getRecords().stream().map(SysConfig::getGroupId)
                .filter(Objects::nonNull).distinct().collect(Collectors.toList());

        Map<Long, String> groupNameMap = new HashMap<>();
        if (CollectionUtils.isNotEmpty(groupIds)) {
            DataGroupQuery groupReq = new DataGroupQuery().setBizType(DataGroupBizTypeEnum.CONFIG.getType())
                    .setIds(groupIds);
            Map<Long, String> dbGroupNameMap = infraApi.listDataGroup(groupReq).stream()
                    .collect(Collectors.toMap(DataGroupDto::getId, DataGroupDto::getName));
            groupNameMap.putAll(dbGroupNameMap);
        }

        List<ConfigResp> respData = pageData.getRecords().stream()
                .map(e -> {
                    ConfigResp resp = configAssembly.toConfigResp(e);
                    resp.setGroupName(groupNameMap.get(e.getGroupId()));
                    return resp;
                })
                .collect(Collectors.toList());
        Page<ConfigResp> resp = new Page<>(pageData.getCurrent(), pageData.getSize(), pageData.getTotal());
        resp.setRecords(respData);
        return resp;
    }

    @Override
    public ConfigResp getConfig(ConfigGetReq req) {
        ConfigKeyEnum.checkExistKey(req.getConfigKey());

        String tenantId = TenantContext.get().getTenantId();
        String cacheKey = String.format(CacheCons.SYS_CONFIG, tenantId, req.getConfigKey());
        Object cacheObj = redisTemplate.opsForValue().get(cacheKey);
        if (Objects.nonNull(cacheObj)) {
            SysConfig entity = (SysConfig) cacheObj;
            entity.setConfigKey(req.getConfigKey());
            ConfigHandler.handlerReadOps(entity);
            return doSensitiveData(entity, req);
        }

        SysConfig entity = configManager.getByConfigKey(req.getConfigKey());
        if (Objects.isNull(entity)) {
            entity = new SysConfig();
            entity.setConfigKey(req.getConfigKey());
            ConfigHandler.handlerReadOps(entity);
            return configAssembly.toConfigResp(entity);
        }

        redisTemplate.opsForValue().set(cacheKey, entity);
        ConfigHandler.handlerReadOps(entity);
        return doSensitiveData(entity, req);
    }

    private ConfigResp doSensitiveData(SysConfig entity, ConfigGetReq req) {
        boolean isVerify = needVerify();
        if (req.getIsDesensitization() && StringUtils.isNotEmpty(entity.getConfigValue())
                && entity.getConfigValue().startsWith("{") && isVerify) {
            ConfigKeyEnum configKeyEnum = ConfigKeyEnum.of(entity.getConfigKey());
            String configValue = BaseConfigParam.processSensitiveData(configKeyEnum, entity.getConfigValue());
            entity.setConfigValue(configValue.replace(" ", ""));
        }
        ConfigResp configResp = configAssembly.toConfigResp(entity);

        Boolean isSensitive = Optional.ofNullable(ConfigKeyEnum.of(req.getConfigKey()))
                .map(ConfigKeyEnum::getIsSensitive).orElse(false);
        configResp.setIsSensitive(isSensitive && isVerify);
        return configResp;
    }

    @Override
    public ConfigResp getSensitiveConfig(SensitiveConfigGetReq req) {
        // 验证一次过后, 30分钟之内无需再验证
        if (needVerify()) {
            // 检验邮箱/手机号验证
            CheckAuthUserCaptchaReq messageReq = new CheckAuthUserCaptchaReq();
            messageReq.setTaskCode(req.getTaskCode());
            messageReq.setVerifyCode(req.getVerifyCode());
            msgPushApi.checkAuthUserCaptcha(messageReq);

            long timeOUt = CacheCons.SHOW_SENSITIVE_CONFIG_TIMEOUT;
            String cacheKey = String.format(CacheCons.SHOW_SENSITIVE_CONFIG,
                SecurityUtils.getLoginUser().getLoginId(),
                TenantContext.get().getTenantId()
            );
            redisTemplate.opsForValue().set(cacheKey, "", timeOUt, TimeUnit.SECONDS);
        }
        authApi.deleteCaptcha(req.getUuid());

        SysConfig config = configManager.getByConfigKey(req.getConfigKey());
        ConfigResp configResp = configAssembly.toConfigResp(config);

        // 这里设置成false, 前端会不展示查看明文按钮
        configResp.setIsSensitive(false);
        return configResp;
    }

    /**
     * 是否需要验证
     * @return true 无需验证, 之前已经验证过了
     */
    private boolean needVerify() {
        String cacheKey = String.format(CacheCons.SHOW_SENSITIVE_CONFIG,
            SecurityUtils.getLoginUser().getLoginId(),
            TenantContext.get().getTenantId()
        );
        return !Boolean.TRUE.equals(redisTemplate.hasKey(cacheKey));
    }

    @Override
    public ConfigInfo getUseConfig(ConfigDefineEnum configDefine) {
        TenantContext tenantContext = TenantContext.get();
        String tenantId = ConfigUtils.getTargetTenantId(configDefine);
        return TenantUtils.execute(DataIsolateLevel.ALL, tenantId, () -> {
            SysConfig useConfig = ConfigHandler.getUseConfig(configDefine);
            if (Objects.isNull(useConfig)) {
                return null;
            }
            ConfigInfo configInfo = new ConfigInfo();
            configInfo.setConfigKey(useConfig.getConfigKey());
            configInfo.setConfigValue(useConfig.getConfigValue());
            configInfo.setTenantId(useConfig.getTenantId());
            return configInfo;
        });
    }

    /**
     * 查询参数配置列表
     * @return 参数配置集合
     */
    @Override
    @FieldBindHandle
    public List<ConfigResp> list(ConfigPageReq req) {
        LocalDateTime beginTime = req.getBeginLocalDateTime();
        LocalDateTime endTime = req.getEndLocalDateTime();
        
        LambdaQueryWrapper<SysConfig> wrapper = SysConfig.lqw()
                .like(StringUtils.isNotEmpty(req.getConfigName()), SysConfig::getConfigName, req.getConfigName())
                .eq(Objects.nonNull(req.getSysBuiltIn()), SysConfig::getSysBuiltIn, req.getSysBuiltIn())
                .like(StringUtils.isNotEmpty(req.getConfigKey()), SysConfig::getConfigKey, req.getConfigKey())
                .ge(Objects.nonNull(beginTime), BasePlusPO::getCreatedAt, beginTime)
                .ne(SysConfig::getIsCustom, true)
                .le(Objects.nonNull(endTime), BasePlusPO::getCreatedAt, endTime)
                .orderByDesc(BasePlusPO::getUpdatedAt);
        
        return this.list(wrapper).stream().map(configAssembly::toConfigResp)
                .collect(Collectors.toList());
    }

    /**
     * 清空参数缓存数据
     */
    @Override
    public void clearConfigCache() {
        String tenantId = TenantContext.get().getTenantId();
        String key = String.format(CacheCons.SYS_CONFIG, tenantId, "*");
        redisTemplate.keys(key);
    }

    /**
     * 重置参数缓存数据
     */
    @Override
    public void resetConfigCache() {
        SpringUtils.getAopProxy(this).clearConfigCache();
    }


    /**
     * 根据主键查询 参数配置
     *
     * @param id 主键
     * @return 返回记录，没有返回null
     */
    @Override
    @FieldBindHandle
    public ConfigResp getConfigById(Long id) {
        SysConfig entity = this.getById(id);
        if (Objects.isNull(entity)) {
            return null;
        }
        return configAssembly.toConfigResp(entity);
    }

    private void validateSaveConfig(ConfigSaveReq req) {
        LambdaQueryWrapper<SysConfig> lqw = SysConfig.lqw().eq(SysConfig::getConfigKey, req.getConfigKey());
        if (this.count(lqw) > 0) {
            throw new BizException("参数配置键值 {} 已存在", req.getConfigKey());
        }
    }

    /**
     * 初始化组, 如果前端没有传, 自动创建一个组, 组编号和配置键名一样
     */
    public void initGroup(ConfigBase req) {
        if (Objects.isNull(req.getGroupId())) {
            DataGroupQuery listReq = new DataGroupQuery();
            listReq.setBizType(DataGroupBizTypeEnum.CONFIG.getType());
            listReq.setCode(CommonCons.DataGroup.DEFAULT_CODE);
            DataGroupDto defaultGroup = infraApi.listDataGroup(listReq).stream().findFirst()
                    .orElseThrow(() -> new BizException("初始化默认组失败"));
            req.setGroupId(defaultGroup.getId());
        }
    }

    /**
     * 保存 参数配置
     *
     * @param req 新增的记录
     * @return 返回影响行数
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean saveConfig(ConfigSaveReq req) {
        String tenantId = TenantContext.get().getTenantId();
        validateSaveConfig(req);

        // 初始化组
        initGroup(req);

        String cacheKey = String.format(CacheCons.SYS_CONFIG, tenantId, req.getConfigKey());
        redisTemplate.delete(cacheKey);
        SysConfig entity = configAssembly.toConfig(req);

        SysConfig copeEntity = new SysConfig();
        BeanUtils.copyProperties(entity, copeEntity);
        entity.setId(null);
        entity.setIsCustom(true);
        boolean ret = configManager.saveConfig(entity);

        ConfigHandler.handlerWriteOps(copeEntity);
        return ret;
    }

    /**
     * 更新 参数配置, 只能更新自定义配置
     *
     * @param req 修改的记录
     * @return 返回影响行数
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean updateConfig(ConfigUpdateReq req) {
        String cacheKey = String.format(CacheCons.SYS_CONFIG, TenantContext.get().getTenantId(), req.getConfigKey());
        redisTemplate.delete(cacheKey);

        // 判断是否只能更新在列表中展示的配置
        LambdaQueryWrapper<SysConfig> lqw = SysConfig.lqw().eq(SysConfig::getId, req.getId());
        SysConfig dbConfig = this.getOne(lqw);
        if (Objects.isNull(dbConfig)) {
            throw new BizException("当前配置不允许更新或者不存在");
        }

        if (!dbConfig.getIsCustom()) {
            throw new BizException("当前接口不支持更新非自定义配置");
        }

        if (Objects.isNull(dbConfig.getGroupId())) {
            initGroup(req);
        }

        SysConfig config = configAssembly.toConfig(req);
        boolean ret = configManager.updateConfig(config);

        SysConfig entity = this.getById(req.getId());
        if (Objects.isNull(entity)) {
            throw new BizException(GlobalErrorEnum.OPERATION_FAIL_SELETE);
        }
        return ret;
    }

    /**
     * 通过id删除 参数配置
     *
     * @param id 删除的主键
     * @return 返回影响行数
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean deleteConfigById(Long id) {
        String tenantId = TenantContext.get().getTenantId();
        SysConfig config = this.getById(id);
        if (Objects.isNull(config)) {
            throw new BizException(GlobalErrorEnum.OPERATION_FAIL_SELETE);
        }
        if (Boolean.TRUE.equals(config.getSysBuiltIn())) {
            throw new BizException("内置参数【{}】不能删除 ", config.getConfigKey());
        }
        boolean ret = this.removeById(id);

        // 判断是否没有引用当前配置组的配置了
        if (Objects.nonNull(config.getGroupId()) &&
                this.count(SysConfig.lqw().eq(SysConfig::getGroupId, config.getGroupId())) == 0) {
            infraApi.deleteDataGroup(DataGroupBizTypeEnum.CONFIG, config.getGroupId());
        }

        String cacheKey = String.format(CacheCons.SYS_CONFIG, tenantId, config.getConfigKey());
        redisTemplate.delete(cacheKey);
        configEventService.publishDeleteConfigEvent(ConfigKeyEnum.of(cacheKey));
        return ret;
    }

    @Override
    public void deleteConfigByKey(String configKey) {
        if (StringUtils.isEmpty(configKey)) {
            return;
        }
        String tenantId = TenantContext.get().getTenantId();
        List<SysConfig> configList = configManager.list(SysConfig.lqw().eq(SysConfig::getConfigKey, configKey));
        if (CollectionUtils.isEmpty(configList)) {
            return;
        }
        boolean ret = this.remove(SysConfig.lqw().eq(SysConfig::getConfigKey, configKey));
        String cacheKey = String.format(CacheCons.SYS_CONFIG, tenantId, configKey);
        redisTemplate.delete(cacheKey);

        configList.forEach(e -> configEventService.publishDeleteConfigEvent(ConfigKeyEnum.of(e.getConfigKey())));
    }

    /**
     * 通过ids删除 参数配置
     *
     * @param ids 删除的主键
     * @return 返回影响行数
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean deleteConfigByIds(List<Long> ids) {
        String tenantId = TenantContext.get().getTenantId();
        for (Long configId : ids) {
            SysConfig config = this.getById(configId);
            if (Objects.isNull(config)) {
                continue;
            }
            if (Boolean.TRUE.equals(config.getSysBuiltIn())) {
                throw new BizException("内置参数【{}】不能删除 ", config.getConfigKey());
            }
            String cacheKey = String.format(CacheCons.SYS_CONFIG, tenantId, config.getConfigKey());
            redisTemplate.delete(cacheKey);
            this.removeById(configId);

            configEventService.publishDeleteConfigEvent(ConfigKeyEnum.of(config.getConfigKey()));
        }
        return true;
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void updateConfigParam(ConfigParamUpdateReq req) {
        String tenantId = TenantContext.get().getTenantId();

        String cacheKey = String.format(CacheCons.SYS_CONFIG, tenantId, req.getConfigKey());
        redisTemplate.delete(cacheKey);
        ConfigKeyEnum configKey = ConfigKeyEnum.of(req.getConfigKey());
        BaseConfigParam.validate(configKey, req.getConfigValue());

        SysConfig config = configAssembly.toConfig(req);

        LambdaQueryWrapper<SysConfig> lqw = SysConfig.lqw()
                .eq(StringUtils.isNotEmpty(req.getConfigKey()), SysConfig::getConfigKey, req.getConfigKey())
                .eq(Objects.nonNull(req.getId()), SysConfig::getId, req.getId());
        SysConfig dbEntity = this.getOne(lqw);
        if (Objects.isNull(dbEntity)) {
            config.setIsCustom(false);
            config.setSysBuiltIn(true);
            config.setConfigName(req.getConfigKey());
        } else if (dbEntity.getConfigValue().startsWith("{")) { // 目前只对对象进行处理
            // 处理经过脱敏处理的字段, 不支持对象嵌套
            // 1. 先解析数据库中存储的 configValue, 并进行脱敏处理
            ConfigKeyEnum configKeyEnum = ConfigKeyEnum.of(dbEntity.getConfigKey());
            String sensitiveRet = BaseConfigParam.processSensitiveData(configKeyEnum, dbEntity.getConfigValue());
            JSONObject sensitiveObj = JSON.parseObject(sensitiveRet);

            // 2. 解析请求的 configValue
            JSONObject oldConfigValue = JSON.parseObject(dbEntity.getConfigValue());
            JSONObject newConfigValue = JSON.parseObject(req.getConfigValue());

            // 3. 将前两步骤中的值进行比对, 如果一样就将数据库中的值赋值到req的configValue中某个字段
            newConfigValue.forEach((k, v) -> {
                if (Objects.equals(String.valueOf(sensitiveObj.get(k)), String.valueOf(v))) {
                    newConfigValue.put(k, oldConfigValue.get(k));
                }
            });
            String targetConfigValue = JSON.toJSONString(newConfigValue);
            config.setConfigValue(targetConfigValue);
            log.info("update config key [{}], new config value [{}]", configKey.getKey(), targetConfigValue);
        }

        ConfigHandler.handlerWriteOps(config);
        this.saveOrUpdate(config, lqw);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void updateStatus(Long configId, String status) {
        // 根据配置查找所在配置组, 将同组下其他配置设置成禁用
        SysConfig config = this.getById(configId);
        if (Objects.isNull(config)) {
            throw new BizException("配置不存在");
        }
        if (Objects.nonNull(config.getGroupId())) {
            LambdaUpdateWrapper<SysConfig> luw = SysConfig.luw().eq(SysConfig::getGroupId, config.getGroupId())
                    .set(SysConfig::getStatus, ConfigCons.Status.DISABLE);
            this.update(luw);
        }
        LambdaUpdateWrapper<SysConfig> luw = SysConfig.luw().eq(SysConfig::getId, config.getId())
                .set(SysConfig::getStatus, status);
        this.update(luw);
    }

    @Override
    public boolean batchDeleteGroup(DataGroupBizTypeEnum bizType, List<Long> ids) {
        DataGroupQuery groupReq = new DataGroupQuery();
        groupReq.setBizType(bizType.getType()).setIds(ids);
        List<DataGroupDto> groupList = infraApi.listDataGroup(groupReq);
        List<Long> groupIds = groupList.stream().map(DataGroupDto::getId).distinct().collect(Collectors.toList());
        if (this.count(SysConfig.lqw().in(SysConfig::getGroupId, groupIds)) > 0) {
            throw new BizException("所删除的配置组下有未删除的配置, 请全部删除后再删除组");
        }

        DataGroupDeleteReq deleteReq = new DataGroupDeleteReq();
        deleteReq.setBizType(bizType.getType()).setIds(ids);
        infraApi.batchDeleteDataGroup(deleteReq);
        return true;
    }
}
